{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Usage\n",
    "\n",
    "**To train a model**: Run 1 ~ 10.\n",
    "\n",
    "**To load model weights**: Run 1 and 4 ~ 7.\n",
    "\n",
    "**To use trained model to swap a single face image**: Run \"**To load model weights**\" and 11.\n",
    "\n",
    "**To use trained model to create a video clips**: Run \"**To load model weights**\", 12 and 13 (or 14).\n",
    "\n",
    "\n",
    "## Index\n",
    "1. [Import Packages](#1)\n",
    "2. [Install Requirements (optional)](#2)\n",
    "3. [Import VGGFace (optional)](#3)\n",
    "4. [Config](#4)\n",
    "5. [Define Models](#5)\n",
    "6. [Load Models](#6)\n",
    "7. [Define Inputs/outputs Variables](#7)\n",
    "8. [Define Loss Function](#8)\n",
    "9. [Utils for loading/displaying images](#9)\n",
    "10. [Start Training](#10)\n",
    "11. [Helper Function: face_swap()](#11)\n",
    "12. [Import Packages for Making Video Clips](#12)\n",
    "13. [Make Video Clips w/o Face Alignment](#13)\n",
    "14. [Make video clips w/ face alignment](#14)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='1'></a>\n",
    "# 1. Import Packages"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.models import Sequential, Model\n",
    "from keras.layers import *\n",
    "from keras.layers.advanced_activations import LeakyReLU\n",
    "from keras.activations import relu\n",
    "from keras.initializers import RandomNormal\n",
    "from keras.applications import *\n",
    "import keras.backend as K\n",
    "from tensorflow.contrib.distributions import Beta\n",
    "import tensorflow as tf\n",
    "from keras.optimizers import Adam"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from image_augmentation import random_transform\n",
    "from image_augmentation import random_warp\n",
    "from utils import get_image_paths, load_images, stack_images\n",
    "from pixel_shuffler import PixelShuffler"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "import numpy as np\n",
    "from PIL import Image\n",
    "import cv2\n",
    "import glob\n",
    "from random import randint, shuffle\n",
    "from IPython.display import clear_output\n",
    "from IPython.display import display\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='2'></a>\n",
    "# 2. Install Requirements (optional)\n",
    "\n",
    "## ========== CAUTION ========== \n",
    "\n",
    "If you are running this jupyter on local machine. Please read [this blog](http://jakevdp.github.io/blog/2017/12/05/installing-python-packages-from-jupyter/) before running the following cells which pip install packages."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# https://github.com/rcmalli/keras-vggface\n",
    "# Skip this cell if you don't want to use perceptual loss\n",
    "!pip install keras_vggface"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We only import ```face_recognition``` and ```moviepy``` when making videos. They are not necessary in training GAN models."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# https://github.com/ageitgey/face_recognition\n",
    "!pip install face_recognition"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install moviepy"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='3'></a>\n",
    "# 3. Import VGGFace (optional)\n",
    "(Skip this part if you don't want to apply perceptual loss)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you got error ```_obtain_input_shape(...)``` error, this is because your keras version is older than vggface requirement. \n",
    "\n",
    "Modify ```_obtain_input_shape(...)``` in ```keras_vggface/models.py``` will solve the problem. The following is what worked for me:\n",
    "\n",
    "```python\n",
    "input_shape = _obtain_input_shape(input_shape,\n",
    "                                  default_size=224,\n",
    "                                  min_size=197,\n",
    "                                  data_format=K.image_data_format(),\n",
    "                                  include_top=include_top)\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras_vggface.vggface import VGGFace"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vggface = VGGFace(include_top=False, model='resnet50', input_shape=(224, 224, 3))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vggface.summary()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='4'></a>\n",
    "# 4. Config\n",
    "\n",
    "mixup paper: https://arxiv.org/abs/1710.09412\n",
    "\n",
    "Default training data directories: `./faceA/` and `./faceB/`\n",
    "\n",
    "Default `use_perceptual_loss = False`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "K.set_learning_phase(1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "channel_axis=-1\n",
    "channel_first = False"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "IMAGE_SHAPE = (64, 64, 3)\n",
    "nc_in = 3 # number of input channels of generators\n",
    "nc_D_inp = 3 # number of input channels of discriminators\n",
    "\n",
    "use_perceptual_loss = False\n",
    "use_lsgan = True\n",
    "use_instancenorm = False\n",
    "use_mixup = True\n",
    "mixup_alpha = 0.1 # 0.2\n",
    "\n",
    "batchSize = 32\n",
    "lrD = 1e-4 # Discriminator learning rate\n",
    "lrG = 1e-4 # Generator learning rate\n",
    "\n",
    "# Path of training images\n",
    "img_dirA = './faceA/*.*'\n",
    "img_dirB = './faceB/*.*'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='5'></a>\n",
    "# 5. Define Models"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "def __conv_init(a):\n",
    "    print(\"conv_init\", a)\n",
    "    k = RandomNormal(0, 0.02)(a) # for convolution kernel\n",
    "    k.conv_weight = True    \n",
    "    return k\n",
    "conv_init = RandomNormal(0, 0.02)\n",
    "gamma_init = RandomNormal(1., 0.02) # for batch normalization"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "#def batchnorm():\n",
    "#    return BatchNormalization(momentum=0.9, axis=channel_axis, epsilon=1.01e-5, gamma_initializer = gamma_init)\n",
    "\n",
    "def conv_block(input_tensor, f):\n",
    "    x = input_tensor\n",
    "    x = Conv2D(f, kernel_size=3, strides=2, kernel_initializer=conv_init, use_bias=False, padding=\"same\")(x)\n",
    "    x = Activation(\"relu\")(x)\n",
    "    return x\n",
    "\n",
    "def conv_block_d(input_tensor, f, use_instance_norm=True):\n",
    "    x = input_tensor\n",
    "    x = Conv2D(f, kernel_size=4, strides=2, kernel_initializer=conv_init, use_bias=False, padding=\"same\")(x)\n",
    "    x = LeakyReLU(alpha=0.2)(x)\n",
    "    return x\n",
    "\n",
    "def res_block(input_tensor, f):\n",
    "    x = input_tensor\n",
    "    x = Conv2D(f, kernel_size=3, kernel_initializer=conv_init, use_bias=False, padding=\"same\")(x)\n",
    "    x = LeakyReLU(alpha=0.2)(x)\n",
    "    x = Conv2D(f, kernel_size=3, kernel_initializer=conv_init, use_bias=False, padding=\"same\")(x)\n",
    "    x = add([x, input_tensor])\n",
    "    x = LeakyReLU(alpha=0.2)(x)\n",
    "    return x\n",
    "\n",
    "# Legacy\n",
    "#def upscale_block(input_tensor, f):\n",
    "#    x = input_tensor\n",
    "#    x = Conv2DTranspose(f, kernel_size=3, strides=2, use_bias=False, kernel_initializer=conv_init)(x) \n",
    "#    x = LeakyReLU(alpha=0.2)(x)\n",
    "#    return x\n",
    "\n",
    "def upscale_ps(filters, use_norm=True):\n",
    "    def block(x):\n",
    "        x = Conv2D(filters*4, kernel_size=3, use_bias=False, kernel_initializer=RandomNormal(0, 0.02), padding='same' )(x)\n",
    "        x = LeakyReLU(0.1)(x)\n",
    "        x = PixelShuffler()(x)\n",
    "        return x\n",
    "    return block\n",
    "\n",
    "def Discriminator(nc_in, input_size=64):\n",
    "    inp = Input(shape=(input_size, input_size, nc_in))\n",
    "    #x = GaussianNoise(0.05)(inp)\n",
    "    x = conv_block_d(inp, 64, False)\n",
    "    x = conv_block_d(x, 128, False)\n",
    "    x = conv_block_d(x, 256, False)\n",
    "    out = Conv2D(1, kernel_size=4, kernel_initializer=conv_init, use_bias=False, padding=\"same\", activation=\"sigmoid\")(x)   \n",
    "    return Model(inputs=[inp], outputs=out)\n",
    "\n",
    "def Encoder(nc_in=3, input_size=64):\n",
    "    inp = Input(shape=(input_size, input_size, nc_in))\n",
    "    x = Conv2D(64, kernel_size=5, kernel_initializer=conv_init, use_bias=False, padding=\"same\")(inp)\n",
    "    x = conv_block(x,128)\n",
    "    x = conv_block(x,256)\n",
    "    x = conv_block(x,512) \n",
    "    x = conv_block(x,1024)\n",
    "    x = Dense(1024)(Flatten()(x))\n",
    "    x = Dense(4*4*1024)(x)\n",
    "    x = Reshape((4, 4, 1024))(x)\n",
    "    out = upscale_ps(512)(x)\n",
    "    return Model(inputs=inp, outputs=out)\n",
    "\n",
    "# Legacy, left for someone to try if interested\n",
    "#def Decoder(nc_in=512, input_size=8):\n",
    "#    inp = Input(shape=(input_size, input_size, nc_in))   \n",
    "#    x = upscale_block(inp, 256)\n",
    "#    x = Cropping2D(((0,1),(0,1)))(x)\n",
    "#    x = upscale_block(x, 128)\n",
    "#    x = res_block(x, 128)\n",
    "#    x = Cropping2D(((0,1),(0,1)))(x)\n",
    "#    x = upscale_block(x, 64)\n",
    "#    x = res_block(x, 64)\n",
    "#    x = res_block(x, 64)\n",
    "#    x = Cropping2D(((0,1),(0,1)))(x)\n",
    "#    x = Conv2D(3, kernel_size=5, kernel_initializer=conv_init, use_bias=False, padding=\"same\")(x)\n",
    "#    out = Activation(\"tanh\")(x)\n",
    "#    return Model(inputs=inp, outputs=out)\n",
    "\n",
    "def Decoder_ps(nc_in=512, input_size=8):\n",
    "    input_ = Input(shape=(input_size, input_size, nc_in))\n",
    "    x = input_\n",
    "    x = upscale_ps(256)(x)\n",
    "    x = upscale_ps(128)(x)\n",
    "    x = upscale_ps(64)(x)\n",
    "    x = res_block(x, 64)\n",
    "    x = res_block(x, 64)\n",
    "    x = Conv2D(3, kernel_size=5, padding='same', activation='tanh' )(x)\n",
    "    return Model(input_, x )    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "encoder = Encoder()\n",
    "decoder_A = Decoder_ps()\n",
    "decoder_B = Decoder_ps()\n",
    "\n",
    "x = Input(shape=IMAGE_SHAPE)\n",
    "\n",
    "netGA = Model(x, decoder_A(encoder(x)))\n",
    "netGB = Model(x, decoder_B(encoder(x)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "encoder.summary()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "decoder_A.summary()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "netDA = Discriminator(nc_D_inp)\n",
    "netDB = Discriminator(nc_D_inp)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='6'></a>\n",
    "# 6. Load Models"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Weights file not found.\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    encoder.load_weights(\"models/encoder.h5\")\n",
    "    decoder_A.load_weights(\"models/decoder_A.h5\")\n",
    "    decoder_B.load_weights(\"models/decoder_B.h5\")\n",
    "    #netDA.load_weights(\"models/netDA.h5\") # uncomment these if you want to continue training from last checkpoint\n",
    "    #netDB.load_weights(\"models/netDB.h5\")\n",
    "    print (\"model loaded.\")\n",
    "except:\n",
    "    print (\"Weights file not found.\")\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='7'></a>\n",
    "# 7. Define Inputs/outputs Variables\n",
    "\n",
    "    distorted_A: A (batch_size, 64, 64, 3) tensor, input of generator_A (netGA).\n",
    "    distorted_B: A (batch_size, 64, 64, 3) tensor, input of generator_B (netGB).\n",
    "    fake_A: (batch_size, 64, 64, 3) tensor, output of generator_A (netGA).\n",
    "    fake_B: (batch_size, 64, 64, 3) tensor, output of generator_B (netGB).\n",
    "    path_A: A function that takes distorted_A as input and outputs fake_A.\n",
    "    path_B: A function that takes distorted_B as input and outputs fake_B.\n",
    "    real_A: A (batch_size, 64, 64, 3) tensor, target images for generator_A given input distorted_A.\n",
    "    real_B: A (batch_size, 64, 64, 3) tensor, target images for generator_B given input distorted_B."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "def cycle_variables(netG):\n",
    "    distorted_input = netG.inputs[0]\n",
    "    fake_output = netG.outputs[0]    \n",
    "    fn_generate = K.function([distorted_input], [fake_output])\n",
    "    return distorted_input, fake_output, fn_generate"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [],
   "source": [
    "distorted_A, fake_A, path_A = cycle_variables(netGA)\n",
    "distorted_B, fake_B, path_B = cycle_variables(netGB)\n",
    "real_A = Input(shape=IMAGE_SHAPE)\n",
    "real_B = Input(shape=IMAGE_SHAPE)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "<a id='8'></a>\n",
    "# 8. Define Loss Function\n",
    "\n",
    "Use LSGAN"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [],
   "source": [
    "if use_lsgan:\n",
    "    loss_fn = lambda output, target : K.mean(K.abs(K.square(output-target)))\n",
    "else:\n",
    "    loss_fn = lambda output, target : -K.mean(K.log(output+1e-12)*target+K.log(1-output+1e-12)*(1-target))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "def define_loss(netD, real, fake, vggface_feat=None):    \n",
    "    if use_mixup:\n",
    "        dist = Beta(mixup_alpha, mixup_alpha)\n",
    "        lam = dist.sample()\n",
    "        mixup = lam * real + (1 - lam) * fake\n",
    "        output_mixup = netD(mixup)\n",
    "        loss_D = loss_fn(output_mixup, lam * K.ones_like(output_mixup)) \n",
    "        output_fake = netD(fake) # dummy\n",
    "        loss_G = .5 * loss_fn(output_mixup, (1 - lam) * K.ones_like(output_mixup))\n",
    "    else:\n",
    "        output_real = netD(real) # positive sample\n",
    "        output_fake = netD(fake) # negative sample   \n",
    "        loss_D_real = loss_fn(output_real, K.ones_like(output_real))    \n",
    "        loss_D_fake = loss_fn(output_fake, K.zeros_like(output_fake))   \n",
    "        loss_D = loss_D_real + loss_D_fake\n",
    "        loss_G = .5 * loss_fn(output_fake, K.ones_like(output_fake))    \n",
    "    loss_G += K.mean(K.abs(fake - real))\n",
    "    \n",
    "    # ========== Perceptual Loss ==========\n",
    "    if not vggface_feat is None:\n",
    "        pl_params = (0.01, 0.1, 0.1)\n",
    "        real_sz224 = tf.image.resize_images(real, [224, 224])\n",
    "        fake_sz224 = tf.image.resize_images(fake, [224, 224])    \n",
    "        real_feat55, real_feat28, real_feat7 = vggface_feat(real_sz224)\n",
    "        fake_feat55, fake_feat28, fake_feat7  = vggface_feat(fake_sz224)    \n",
    "        loss_G += pl_params[0] * K.mean(K.abs(fake_feat7 - real_feat7))\n",
    "        loss_G += pl_params[1] * K.mean(K.abs(fake_feat28 - real_feat28))\n",
    "        loss_G += pl_params[2] * K.mean(K.abs(fake_feat55 - real_feat55))\n",
    "    \n",
    "    return loss_D, loss_G"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ========== Define Perceptual Loss Model==========\n",
    "if use_perceptual_loss:\n",
    "    vggface.trainable = False\n",
    "    out_size55 = vggface.layers[36].output\n",
    "    out_size28 = vggface.layers[78].output\n",
    "    out_size7 = vggface.layers[-2].output\n",
    "    vggface_feat = Model(vggface.input, [out_size55, out_size28, out_size7])\n",
    "    vggface_feat.trainable = False\n",
    "else:\n",
    "    vggface_feat = None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "outputs": [],
   "source": [
    "loss_DA, loss_GA = define_loss(netDA, real_A, fake_A, vggface_feat)\n",
    "loss_DB, loss_GB = define_loss(netDB, real_B, fake_B, vggface_feat)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "weightsDA = netDA.trainable_weights\n",
    "weightsGA = netGA.trainable_weights\n",
    "weightsDB = netDB.trainable_weights\n",
    "weightsGB = netGB.trainable_weights\n",
    "\n",
    "# Adam(..).get_updates(...)\n",
    "training_updates = Adam(lr=lrD, beta_1=0.5).get_updates(weightsDA,[],loss_DA)\n",
    "netDA_train = K.function([distorted_A, real_A],[loss_DA], training_updates)\n",
    "training_updates = Adam(lr=lrG, beta_1=0.5).get_updates(weightsGA,[], loss_GA)\n",
    "netGA_train = K.function([distorted_A, real_A], [loss_GA], training_updates)\n",
    "\n",
    "training_updates = Adam(lr=lrD, beta_1=0.5).get_updates(weightsDB,[],loss_DB)\n",
    "netDB_train = K.function([distorted_B, real_B],[loss_DB], training_updates)\n",
    "training_updates = Adam(lr=lrG, beta_1=0.5).get_updates(weightsGB,[], loss_GB)\n",
    "netGB_train = K.function([distorted_B, real_B], [loss_GB], training_updates)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "<a id='8'></a>\n",
    "# 9. Utils for loading/displaying images"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "outputs": [],
   "source": [
    "def load_data(file_pattern):\n",
    "    return glob.glob(file_pattern)\n",
    "\n",
    "random_transform_args = {\n",
    "    'rotation_range': 20,\n",
    "    'zoom_range': 0.1,\n",
    "    'shift_range': 0.05,\n",
    "    'random_flip': 0.5,\n",
    "    }\n",
    "def read_image(fn, random_transform_args=random_transform_args):\n",
    "    image = cv2.imread(fn)\n",
    "    image = cv2.resize(image, (256,256)) / 255 * 2 - 1\n",
    "    image = random_transform( image, **random_transform_args )\n",
    "    warped_img, target_img = random_warp( image )\n",
    "    \n",
    "    return warped_img, target_img"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {},
   "outputs": [],
   "source": [
    "# A generator function that yields epoch, batchsize of warped_img and batchsize of target_img\n",
    "def minibatch(data, batchsize):\n",
    "    length = len(data)\n",
    "    epoch = i = 0\n",
    "    tmpsize = None  \n",
    "    shuffle(data)\n",
    "    while True:\n",
    "        size = tmpsize if tmpsize else batchsize\n",
    "        if i+size > length:\n",
    "            shuffle(data)\n",
    "            i = 0\n",
    "            epoch+=1        \n",
    "        rtn = np.float32([read_image(data[j]) for j in range(i,i+size)])\n",
    "        i+=size\n",
    "        tmpsize = yield epoch, rtn[:,0,:,:,:], rtn[:,1,:,:,:]       \n",
    "\n",
    "def minibatchAB(dataA, batchsize):\n",
    "    batchA = minibatch(dataA, batchsize)\n",
    "    tmpsize = None    \n",
    "    while True:        \n",
    "        ep1, warped_img, target_img = batchA.send(tmpsize)\n",
    "        tmpsize = yield ep1, warped_img, target_img"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def showG(test_A, test_B, path_A, path_B):\n",
    "    figure_A = np.stack([\n",
    "        test_A,\n",
    "        np.squeeze(np.array([path_A([test_A[i:i+1]]) for i in range(test_A.shape[0])])),\n",
    "        np.squeeze(np.array([path_B([test_A[i:i+1]]) for i in range(test_A.shape[0])])),\n",
    "        ], axis=1 )\n",
    "    figure_B = np.stack([\n",
    "        test_B,\n",
    "        np.squeeze(np.array([path_B([test_B[i:i+1]]) for i in range(test_B.shape[0])])),\n",
    "        np.squeeze(np.array([path_A([test_B[i:i+1]]) for i in range(test_B.shape[0])])),\n",
    "        ], axis=1 )\n",
    "\n",
    "    figure = np.concatenate([figure_A, figure_B], axis=0 )\n",
    "    figure = figure.reshape((4,7) + figure.shape[1:])\n",
    "    figure = stack_images(figure)\n",
    "    figure = np.clip((figure + 1) * 255 / 2, 0, 255).astype('uint8')\n",
    "    figure = cv2.cvtColor(figure, cv2.COLOR_BGR2RGB)\n",
    "\n",
    "    display(Image.fromarray(figure))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='10'></a>\n",
    "# 10. Start Training\n",
    "\n",
    "Show results and save model weights every `display_iters` iterations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!mkdir models # create ./models directory"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Get filenames\n",
    "train_A = load_data(img_dirA)\n",
    "train_B = load_data(img_dirB)\n",
    "\n",
    "assert len(train_A), \"No image found in \" + str(img_dirA)\n",
    "assert len(train_B), \"No image found in \" + str(img_dirB)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t0 = time.time()\n",
    "niter = 150\n",
    "gen_iterations = 0\n",
    "epoch = 0\n",
    "errGA_sum = errGB_sum = errDA_sum = errDB_sum = 0\n",
    "\n",
    "display_iters = 50\n",
    "train_batchA = minibatchAB(train_A, batchSize)\n",
    "train_batchB = minibatchAB(train_B, batchSize)\n",
    "\n",
    "#while epoch < niter: \n",
    "while gen_iterations < 20000:\n",
    "    epoch, warped_A, target_A = next(train_batchA) \n",
    "    epoch, warped_B, target_B = next(train_batchB) \n",
    "    \n",
    "    # Train dicriminators for one batch\n",
    "    if gen_iterations % 1 == 0:\n",
    "        errDA  = netDA_train([warped_A, target_A])\n",
    "        errDB  = netDB_train([warped_B, target_B])\n",
    "    errDA_sum +=errDA[0]\n",
    "    errDB_sum +=errDB[0]\n",
    "    \n",
    "    if gen_iterations == 5:\n",
    "        print (\"working.\")\n",
    "\n",
    "    # Train generators for one batch\n",
    "    errGA = netGA_train([warped_A, target_A])\n",
    "    errGB = netGB_train([warped_B, target_B])\n",
    "    errGA_sum += errGA[0]\n",
    "    errGB_sum += errGB[0]\n",
    "    gen_iterations+=1\n",
    "    \n",
    "    if gen_iterations % display_iters == 0:\n",
    "        if gen_iterations % (3*display_iters) == 0: # clear_output every 3*display_iters iters\n",
    "            clear_output()\n",
    "        print('[%d/%d][%d] Loss_DA: %f Loss_DB: %f Loss_GA: %f Loss_GB: %f time: %f'\n",
    "        % (epoch, niter, gen_iterations, errDA_sum/display_iters, errDB_sum/display_iters,\n",
    "           errGA_sum/display_iters, errGB_sum/display_iters, time.time()-t0))   \n",
    "        \n",
    "        # get new batch of images and generate results for visualization\n",
    "        _, wA, tA = train_batchA.send(14)  \n",
    "        _, wB, tB = train_batchB.send(14)\n",
    "        showG(tA, tB, path_A, path_B)        \n",
    "        errGA_sum = errGB_sum = errDA_sum = errDB_sum = 0\n",
    "        \n",
    "        # Save models\n",
    "        encoder.save_weights(\"models/encoder.h5\")\n",
    "        decoder_A.save_weights(\"models/decoder_A.h5\" )\n",
    "        decoder_B.save_weights(\"models/decoder_B.h5\" )\n",
    "        netDA.save_weights(\"models/netDA.h5\")\n",
    "        netDB.save_weights(\"models/netDB.h5\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='11'></a>\n",
    "# 11. Helper Function: face_swap()\n",
    "This function is provided for those who don't have enough VRAM to run dlib's cnn and GAN model at the same time.\n",
    "\n",
    "    INPUTS:\n",
    "        img: A RGB face image of any size\n",
    "        path_func: a function that is either path_A or path_B\n",
    "    OUPUT:\n",
    "        result_img: A RGB swapped face image"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def swap_face(img, path_func):\n",
    "    input_size = img.shape\n",
    "    img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) # generator expects BGR input    \n",
    "    ae_input = cv2.resize(img, (64,64))/255. * 2 - 1 # resize img to 64x64 and normalize it     \n",
    "    \n",
    "    result = np.squeeze(np.array([path_func([[ae_input]])]))\n",
    "    \n",
    "    result = np.clip( (result + 1) * 255 / 2, 0, 255 ).astype('uint8')   \n",
    "    result = cv2.cvtColor(result, cv2.COLOR_BGR2RGB) \n",
    "    result = cv2.resize(result, (input_size[1],input_size[0]))\n",
    "    return result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "whom2whom = \"BtoA\" # default trainsforming faceB to faceA\n",
    "\n",
    "if whom2whom is \"AtoB\":\n",
    "    path_func = path_B\n",
    "elif whom2whom is \"BtoA\":\n",
    "    path_func = path_A\n",
    "else:\n",
    "    print (\"whom2whom should be either AtoB or BtoA\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "input_img = plt.imread(\"./IMAGE_FILENAME.jpg\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.imshow(input_img)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "result_img = swap_face(input_img, path_func)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.imshow(result_img)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='12'></a>\n",
    "# 12. Import Packages for Making Video Clips\n",
    "\n",
    "Given a video as input, the following cells will detect face for each frame using dlib's cnn model. And use trained GAN model to transform detected face into target face. Then output a video with swapped faces."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Imageio: 'ffmpeg-linux64-v3.3.1' was not found on your computer; downloading it now.\n",
      "Try 1. Download from https://github.com/imageio/imageio-binaries/raw/master/ffmpeg/ffmpeg-linux64-v3.3.1 (43.8 MB)\n",
      "Downloading: 8192/45929032 bytes (0.03170304/45929032 bytes (6.9%7176192/45929032 bytes (15.611288576/45929032 bytes (24.6%15360000/45929032 bytes (33.4%19374080/45929032 bytes (42.2%23412736/45929032 bytes (51.0%27435008/45929032 bytes (59.7%31555584/45929032 bytes (68.7%35676160/45929032 bytes (77.7%39772160/45929032 bytes (86.6%43802624/45929032 bytes (95.4%45929032/45929032 bytes (100.0%)\n",
      "  Done\n",
      "File saved as /root/.imageio/ffmpeg/ffmpeg-linux64-v3.3.1.\n"
     ]
    }
   ],
   "source": [
    "# Download ffmpeg if need, which is required by moviepy.\n",
    "\n",
    "#import imageio\n",
    "#imageio.plugins.ffmpeg.download()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [],
   "source": [
    "import face_recognition\n",
    "from moviepy.editor import VideoFileClip"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='13'></a>\n",
    "# 13. Make Video Clips w/o Face Alignment"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 77,
   "metadata": {},
   "outputs": [],
   "source": [
    "use_smoothed_mask = True\n",
    "use_smoothed_bbox = True\n",
    "\n",
    "#def get_gen_output(inp, path):\n",
    "#    return np.squeeze(np.array([path([[inp]])]))\n",
    "\n",
    "def get_smoothed_coord(x0, x1, y0, y1):\n",
    "    global prev_x0, prev_x1, prev_y0, prev_y1\n",
    "    x0 = int(0.65*prev_x0 + 0.35*x0)\n",
    "    x1 = int(0.65*prev_x1 + 0.35*x1)\n",
    "    y1 = int(0.65*prev_y1 + 0.35*y1)\n",
    "    y0 = int(0.65*prev_y0 + 0.35*y0)\n",
    "    return x0, x1, y0, y1    \n",
    "    \n",
    "def set_global_coord(x0, x1, y0, y1):\n",
    "    global prev_x0, prev_x1, prev_y0, prev_y1\n",
    "    prev_x0 = x0\n",
    "    prev_x1 = x1\n",
    "    prev_y1 = y1\n",
    "    prev_y0 = y0\n",
    "\n",
    "def process_video(input_img):   \n",
    "    image = input_img\n",
    "    faces = face_recognition.face_locations(image, model=\"cnn\")\n",
    "    \n",
    "    if len(faces) == 0:\n",
    "        comb_img = np.zeros([input_img.shape[0], input_img.shape[1]*2,input_img.shape[2]])\n",
    "        comb_img[:, :input_img.shape[1], :] = input_img\n",
    "        comb_img[:, input_img.shape[1]:, :] = input_img\n",
    "    \n",
    "    global prev_x0, prev_x1, prev_y0, prev_y1\n",
    "    global frames    \n",
    "    for (x0, y1, x1, y0) in faces:\n",
    "        h = x1 - x0\n",
    "        w = y1 - y0\n",
    "        \n",
    "        # smoothing bounding box\n",
    "        if use_smoothed_bbox:\n",
    "            if frames != 0:\n",
    "                x0, x1, y0, y1 = get_smoothed_coord(x0, x1, y0, y1)\n",
    "                set_global_coord(x0, x1, y0, y1)\n",
    "            else:\n",
    "                set_global_coord(x0, x1, y0, y1)\n",
    "                frames += 1\n",
    "            \n",
    "        cv2_img = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)\n",
    "        roi_image = cv2_img[x0+h//15:x1-h//15,y0+w//15:y1-w//15,:]\n",
    "        roi_size = roi_image.shape  \n",
    "        \n",
    "        # smoothing mask\n",
    "        if use_smoothed_mask:\n",
    "            mask = np.zeros_like(roi_image)\n",
    "            mask[h//15:-h//15,w//15:-w//15,:] = 255\n",
    "            mask = cv2.GaussianBlur(mask,(15,15),10)\n",
    "            orig_img = cv2.cvtColor(roi_image, cv2.COLOR_BGR2RGB)\n",
    "        \n",
    "        ae_input = cv2.resize(roi_image, (64,64))/255. * 2 - 1        \n",
    "        result = np.squeeze(np.array([path_A([[ae_input]])])) # Change path_A/path_B here\n",
    "        result = np.clip( (result + 1) * 255 / 2, 0, 255 ).astype('uint8')\n",
    "        result = cv2.cvtColor(result, cv2.COLOR_BGR2RGB)\n",
    "        result = cv2.resize(result, (roi_size[1],roi_size[0]))\n",
    "        comb_img = np.zeros([input_img.shape[0], input_img.shape[1]*2,input_img.shape[2]])\n",
    "        comb_img[:, :input_img.shape[1], :] = input_img\n",
    "        comb_img[:, input_img.shape[1]:, :] = input_img\n",
    "        \n",
    "        if use_smoothed_mask:\n",
    "            comb_img[x0+h//15:x1-h//15, input_img.shape[1]+y0+w//15:input_img.shape[1]+y1-w//15,:] = mask/255*result + (1-mask/255)*orig_img\n",
    "        else:\n",
    "            comb_img[x0+h//15:x1-h//15, input_img.shape[1]+y0+w//15:input_img.shape[1]+y1-w//15,:] = result\n",
    "    \n",
    "    return comb_img"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 78,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[MoviePy] >>>> Building video result_smoothed_sh_test_clipped2.mp4\n",
      "[MoviePy] Writing video result_smoothed_sh_test_clipped2.mp4\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|█████████▉| 900/901 [02:35<00:00,  5.84it/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[MoviePy] Done.\n",
      "[MoviePy] >>>> Video ready: result_smoothed_sh_test_clipped2.mp4 \n",
      "\n",
      "CPU times: user 2min 23s, sys: 13 s, total: 2min 36s\n",
      "Wall time: 2min 36s\n"
     ]
    }
   ],
   "source": [
    "# Variables for smoothing bounding box\n",
    "global prev_x0, prev_x1, prev_y0, prev_y1\n",
    "global frames\n",
    "prev_x0 = prev_x1 = prev_y0 = prev_y1 = 0\n",
    "frames = 0\n",
    "\n",
    "output = 'OUTPUT_VIDEO.mp4'\n",
    "clip1 = VideoFileClip(\"TEST_VIDEO.mp4\")\n",
    "clip = clip1.fl_image(process_video)#.subclip(11, 13) #NOTE: this function expects color images!!\n",
    "%time clip.write_videofile(output, audio=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<a id='14'></a>\n",
    "# 14. Make video clips w/ face alignment\n",
    "\n",
    "The code is not refined. Also I can't tell if face alignment improves the result.\n",
    "\n",
    "Code reference: https://github.com/nlhkh/face-alignment-dlib"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 72,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1340"
      ]
     },
     "execution_count": 72,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import gc\n",
    "gc.collect()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 140,
   "metadata": {},
   "outputs": [],
   "source": [
    "use_smoothed_mask = True\n",
    "apply_face_aln = True\n",
    "use_poisson_blending = False # SeamlessClone is NOT recommended for video.\n",
    "use_comp_video = True # output a comparison video before/after face swap\n",
    "use_smoothed_bbox = True\n",
    "\n",
    "def get_smoothed_coord(x0, x1, y0, y1):\n",
    "    global prev_x0, prev_x1, prev_y0, prev_y1\n",
    "    x0 = int(0.65*prev_x0 + 0.35*x0)\n",
    "    x1 = int(0.65*prev_x1 + 0.35*x1)\n",
    "    y1 = int(0.65*prev_y1 + 0.35*y1)\n",
    "    y0 = int(0.65*prev_y0 + 0.35*y0)\n",
    "    return x0, x1, y0, y1    \n",
    "    \n",
    "def set_global_coord(x0, x1, y0, y1):\n",
    "    global prev_x0, prev_x1, prev_y0, prev_y1\n",
    "    prev_x0 = x0\n",
    "    prev_x1 = x1\n",
    "    prev_y1 = y1\n",
    "    prev_y0 = y0\n",
    "    \n",
    "def extract_eye_center(shape):\n",
    "    xs = 0\n",
    "    ys = 0\n",
    "    for pnt in shape:\n",
    "        xs += pnt[0]\n",
    "        ys += pnt[1]\n",
    "    return ((xs//6), ys//6)\n",
    "\n",
    "def get_rotation_matrix(p1, p2):\n",
    "    angle = angle_between_2_points(p1, p2)\n",
    "    x1, y1 = p1\n",
    "    x2, y2 = p2\n",
    "    xc = (x1 + x2) // 2\n",
    "    yc = (y1 + y2) // 2\n",
    "    M = cv2.getRotationMatrix2D((xc, yc), angle, 1)\n",
    "    return M, (xc, yc), angle\n",
    "\n",
    "def angle_between_2_points(p1, p2):\n",
    "    x1, y1 = p1\n",
    "    x2, y2 = p2\n",
    "    if x1 == x2:\n",
    "        return 90\n",
    "    tan = (y2 - y1) / (x2 - x1)\n",
    "    return np.degrees(np.arctan(tan))\n",
    "\n",
    "def get_rotated_img(img, det):\n",
    "    #print (det, img.shape)\n",
    "    shape = face_recognition.face_landmarks(img, det)\n",
    "    pnts_left_eye = shape[0][\"left_eye\"]\n",
    "    pnts_right_eye = shape[0][\"right_eye\"]\n",
    "    if len(pnts_left_eye) == 0 or len(pnts_right_eye) == 0:\n",
    "        return img, None, None    \n",
    "    le_center = extract_eye_center(shape[0][\"left_eye\"])\n",
    "    re_center = extract_eye_center(shape[0][\"right_eye\"])\n",
    "    M, center, angle = get_rotation_matrix(le_center, re_center)\n",
    "    M_inv = cv2.getRotationMatrix2D(center, -1*angle, 1)    \n",
    "    rotated = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]), flags=cv2.INTER_CUBIC)    \n",
    "    return rotated, M, M_inv, center\n",
    "\n",
    "def process_video(input_img):   \n",
    "    image = input_img\n",
    "    # ========== Decrease image size if getting memory error ==========\n",
    "    #image = input_img[:3*input_img.shape[0]//4, :, :]\n",
    "    #image = cv2.resize(image, (image.shape[1]//2,image.shape[0]//2))\n",
    "    orig_image = np.array(image)\n",
    "    faces = face_recognition.face_locations(image, model=\"cnn\")\n",
    "    \n",
    "    if len(faces) == 0:\n",
    "        comb_img = np.zeros([orig_image.shape[0], orig_image.shape[1]*2,orig_image.shape[2]])\n",
    "        comb_img[:, :orig_image.shape[1], :] = orig_image\n",
    "        comb_img[:, orig_image.shape[1]:, :] = orig_image\n",
    "        if use_comp_video:\n",
    "            return comb_img\n",
    "        else:\n",
    "            return image\n",
    "    \n",
    "    global prev_x0, prev_x1, prev_y0, prev_y1\n",
    "    global frames\n",
    "    for (x0, y1, x1, y0) in faces:        \n",
    "        # smoothing bounding box\n",
    "        if use_smoothed_bbox:\n",
    "            if frames != 0:\n",
    "                x0, x1, y0, y1 = get_smoothed_coord(x0, x1, y0, y1)\n",
    "                set_global_coord(x0, x1, y0, y1)\n",
    "            else:\n",
    "                set_global_coord(x0, x1, y0, y1)\n",
    "                frames += 1\n",
    "        \n",
    "        h = x1 - x0\n",
    "        w = y1 - y0\n",
    "                \n",
    "        if apply_face_aln:\n",
    "            do_back_rot = True\n",
    "            image, M, M_inv, center = get_rotated_img(image, [(x0, y1, x1, y0)])\n",
    "            if M is None:\n",
    "                do_back_rot = False\n",
    "        \n",
    "        cv2_img = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) \n",
    "        roi_image = cv2_img[x0+h//15:x1-h//15, y0+w//15:y1-w//15, :]\n",
    "        roi_size = roi_image.shape            \n",
    "        \n",
    "        if use_smoothed_mask:\n",
    "            mask = np.zeros_like(roi_image)\n",
    "            #print (roi_image.shape, mask.shape)\n",
    "            mask[h//15:-h//15,w//15:-w//15,:] = 255\n",
    "            mask = cv2.GaussianBlur(mask,(15,15),10)\n",
    "            roi_image_rgb = cv2.cvtColor(roi_image, cv2.COLOR_BGR2RGB)\n",
    "        \n",
    "        ae_input = cv2.resize(roi_image, (64,64))/255. * 2 - 1        \n",
    "        result = np.squeeze(np.array([path_A([[ae_input]])]))\n",
    "        result = np.clip( (result + 1) * 255 / 2, 0, 255 ).astype('uint8')        \n",
    "        result = cv2.cvtColor(result, cv2.COLOR_BGR2RGB)\n",
    "        result = cv2.resize(result, (roi_size[1],roi_size[0]))        \n",
    "        result_img = np.array(orig_image)\n",
    "        \n",
    "        if use_smoothed_mask and not use_poisson_blending:\n",
    "            image[x0+h//15:x1-h//15, y0+w//15:y1-w//15,:] = mask/255*result + (1-mask/255)*roi_image_rgb\n",
    "        elif use_poisson_blending:\n",
    "            c = (y0+w//2, x0+h//2)\n",
    "            image = cv2.seamlessClone(result, image, mask, c, cv2.NORMAL_CLONE)     \n",
    "            \n",
    "        if do_back_rot:\n",
    "            image = cv2.warpAffine(image, M_inv, (image.shape[1], image.shape[0]), flags=cv2.INTER_CUBIC)\n",
    "            result_img[x0+h//15:x1-h//15, y0+w//15:y1-w//15,:] = image[x0+h//15:x1-h//15, y0+w//15:y1-w//15,:]\n",
    "        else:\n",
    "            result_img[x0+h//15:x1-h//15, y0+w//15:y1-w//15,:] = image[x0+h//15:x1-h//15, y0+w//15:y1-w//15,:]   \n",
    "\n",
    "        if use_comp_video:\n",
    "            comb_img = np.zeros([orig_image.shape[0], orig_image.shape[1]*2,orig_image.shape[2]])\n",
    "            comb_img[:, :orig_image.shape[1], :] = orig_image\n",
    "            comb_img[:, orig_image.shape[1]:, :] = result_img\n",
    "            \n",
    "    if use_comp_video:\n",
    "        return comb_img\n",
    "    else:\n",
    "        return result_img"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 141,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[MoviePy] >>>> Building video PL_fa_1to5_smoothed_sh_test3.mp4\n",
      "[MoviePy] Writing video PL_fa_1to5_smoothed_sh_test3.mp4\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 99%|█████████▉| 120/121 [00:22<00:00,  5.22it/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[MoviePy] Done.\n",
      "[MoviePy] >>>> Video ready: PL_fa_1to5_smoothed_sh_test3.mp4 \n",
      "\n",
      "CPU times: user 23.2 s, sys: 1.95 s, total: 25.1 s\n",
      "Wall time: 23.3 s\n"
     ]
    }
   ],
   "source": [
    "# Variables for smoothing bounding box\n",
    "global prev_x0, prev_x1, prev_y0, prev_y1\n",
    "global frames\n",
    "prev_x0 = prev_x1 = prev_y0 = prev_y1 = 0\n",
    "frames = 0\n",
    "\n",
    "output = 'OUTPUT_VIDEO.mp4'\n",
    "clip1 = VideoFileClip(\"TEST_VIDEO.mp4\")\n",
    "# .subclip(START_SEC, END_SEC) for testing\n",
    "clip = clip1.fl_image(process_video)#.subclip(1, 5) #NOTE: this function expects color images!!\n",
    "%time clip.write_videofile(output, audio=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Neptune",
   "language": "",
   "name": "neptune-kernel"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
